Skip to content

feat(chat): OAuth 2.1 pivot + auth_request UX (PR-B、stacked on #19)#21

Merged
shomatan merged 8 commits into
mainfrom
feat/oauth-pivot
May 1, 2026
Merged

feat(chat): OAuth 2.1 pivot + auth_request UX (PR-B、stacked on #19)#21
shomatan merged 8 commits into
mainfrom
feat/oauth-pivot

Conversation

@shomatan
Copy link
Copy Markdown
Contributor

@shomatan shomatan commented Apr 28, 2026

Summary

PR #18 を 4 PR に割り直したうちの OAuth 2.1 ピボット + auth_request UX 部分。base = #19 (PR-A) にスタック。

PR-A の PAT 認証前提を撤回し、MCP プロトコル OAuth 2.1 / SDK 任せの実装に切替える。同時に、SDK の `mcp____authenticate` tool_use を生のまま並べると UX が破綻する (URL がプレーンテキスト + redirect 先 localhost:XXXXX が即死) 問題を、専用 `auth_request` ブロック + `AuthRequestCard` で 1 等地化する。

変更内容

`3f7e63f` refactor: OAuth 2.1 採用 / PAT 排除

Premise 9 撤回:

  • core: `McpServerConfigSchema` から auth フィールド削除、関連 hardening も削除
  • ai-engine: `buildMcpServers` の auth header 構築ロジック削除、url のみで HTTP config
  • frontend: 設定 dialog から auth scheme dropdown / envVar 入力欄を削除

`1f8c676` feat(chat): auth_request UX

  • core: `ChatBlock` に `auth_request` variant 追加 (`status` × `failureMessage` の整合は superRefine で固定)
  • ai-engine: `auth-detector.ts` 新設 (パターン分解 + URL 抽出、末尾句読点 strip)、`stream.ts` に `chat_auth_request` event、`chat-runner` に `handleAuthToolResult` / `findLatestPendingAuthRequest` 追加
  • frontend: `AuthRequestCard` 新設 (認証ボタン + paste 入力 + 状態 badge)、`chat-message` で dispatch、`store` で event 反映

`9e2679d` test: route.test.ts PATCH assert

事前登録 PATCH の status を assert (CR Minor 指摘)。

テスト

```
pnpm typecheck # 4/4 PASS
pnpm test # 91 + 88 + 209 + 272 = 660 PASS
```

残課題

  • ChatRunner は per-turn `sdk.query()` のままなので「auth 1 回 → 同 thread の
    次 turn で再 auth が必要」が残る (SDK subprocess が turn ごとに再生成され
    OAuth state が引き継がれない)。これは PR-C (ChatRunner long-lived 化) で対応。

Stack

Test plan

  • typecheck
  • unit test (660 件)
  • browser smoke test: 新規 chat thread → authenticate ボタンが auth_request カードで出る
  • callback URL paste → AI が complete_authentication 呼ぶ → status 完了に更新

Summary by CodeRabbit

  • New Features

    • OAuth 2.1 認証フロー対応:チャット内で認証リクエストを表示し、認証URLを開いてコールバックURLを送信可能に
    • 認証カード(未認証/完了/失敗)を表示し、完了時は状態更新と失敗メッセージ表示を行う
  • Chores

    • プロジェクト設定からPAT/Basicなどのシークレット入力を削除し、認証はMCP側に委譲
  • Bug Fixes

    • 空のアシスタント応答をプレースホルダで防止

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Warning

Rate limit exceeded

@shomatan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 40 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59a732d4-e537-4426-bd1e-5201a69b39c8

📥 Commits

Reviewing files that changed from the base of the PR and between 6057ac4 and 915d807.

📒 Files selected for processing (2)
  • packages/ai-engine/src/chat-runner.ts
  • packages/ai-engine/src/server.ts
📝 Walkthrough

Walkthrough

MCPサーバーのPAT/Basic認証設定を削除し、OAuth 2.1フロー検出・処理を追加。認証リクエストを表すチャットブロック/イベントとフロントエンドのUI・送信経路を導入し、SDK向けのMCP設定はURLのみを渡すように変更しました。

Changes

Cohort / File(s) Summary
Auth detection
packages/ai-engine/src/auth-detector.ts, packages/ai-engine/src/auth-detector.test.ts
新規モジュール:OAuth関連ツール名の解析(authenticate/complete_authentication)と、ツール出力から認可URLを抽出するロジック(折返し行の再結合、末尾句読点トリム)を追加。
Chat runner & stream
packages/ai-engine/src/chat-runner.ts, packages/ai-engine/src/chat-runner.test.ts, packages/ai-engine/src/stream.ts
ツール使用を検出してauth_requestブロックへ変換、chat_auth_requestイベントを発行。pending→completed/failedの遷移処理、イベントによるブロック更新、空のアシスタントバブル防止、runOAuthCallbackの追加。テスト更新を含む。
Agent / MCP integration tests
packages/ai-engine/src/agent-runner.test.ts, packages/ai-engine/src/mcp/build-mcp-servers.test.ts
外部MCPはURLのみで渡す挙動へテストを変更(Authorization/PAT期待の削除)。env-var依存テストの一部削除。
MCP server builder
packages/ai-engine/src/mcp/build-mcp-servers.ts
環境変数からの認証ヘッダー生成ロジックを削除。SDK向けサーバ設定はtype: 'http'urlのみ(headersを含めない)に変更。
Schema & validation
packages/core/src/schema.ts, packages/core/src/schema.test.ts
McpServerConfigSchemaからauthフィールドを除去。ChatBlockSchemaauth_requestブロックを追加(mcpServerId/mcpServerLabel/authUrl/status/failureMessageのバリデーション)。テスト更新あり。
Frontend - Auth UI
packages/frontend/src/components/chat/auth-request-card.tsx, packages/frontend/src/components/chat/auth-request-card.test.tsx, packages/frontend/src/components/chat/chat-message.tsx
AuthRequestCardを追加:pendingで外部authUrlを開き、ローカルコールバックURLを検証して送信するUI。completed/failed表示とfailureMessage表示対応。チャット描画にauth_request対応を追加。
Frontend - Project settings UI
packages/frontend/src/components/dialog/project-settings-dialog.tsx, packages/frontend/src/components/dialog/project-settings-dialog.test.tsx
プロジェクト設定から認証スキーム/env入力を削除し、AtlassianテンプレートはURLのみ保持するUIへ変更。テスト更新。
Frontend state & WS
packages/frontend/src/lib/store.ts, packages/frontend/src/lib/ws.ts, packages/frontend/src/app/api/projects/[id]/route.test.ts
applyChatEventchat_auth_requestハンドラを追加(pending追加、completed/failedで既存保留ブロック更新)。sendOAuthCallbackストアアクションとWSのChatHandle.sendOAuthCallbackを追加。APIテストでmcpServers.auth除去。
Server - chat endpoint
packages/ai-engine/src/server.ts
WebSocketチャットの新フレームoauth_callbackを追加。ZodでmcpServerIdcallbackUrlを検証し、runner.runOAuthCallbackのイベントをクライアントへ逐次送信。

Sequence Diagram(s)

sequenceDiagram
    participant User as フロントエンドユーザ
    participant Front as Frontend UI
    participant WS as Frontend WS / ChatHandle
    participant Backend as Backend (chat-runner)
    participant SDK as SDK/Agent
    participant MCP as 外部MCPサーバ

    User->>Front: メッセージ送信(OAuth開始)
    Front->>WS: メッセージ送信
    WS->>Backend: stream/query to SDK
    Backend->>SDK: SDKにクエリ(MCP URLのみ)
    SDK->>MCP: MCPへ問い合わせ(認可フロー開始)
    MCP->>SDK: tool_use: authenticate (authUrl)
    SDK->>Backend: stream tool_use
    Backend->>Backend: auth-detectorで解析、chat_auth_request(pending)発行
    Backend->>WS: chat_auth_request イベント
    WS->>Front: イベント配信
    Front->>User: AuthRequestCard表示(authUrlリンク + コールバック入力)
    User->>Front: ローカルコールバックURLを貼付・送信
    Front->>WS: oauth_callback frame (mcpServerId, callbackUrl)
    WS->>Backend: oauth_callback受信
    Backend->>SDK: runOAuthCallback -> complete_authentication with callback
    SDK->>MCP: コールバックをMCPに転送
    MCP->>SDK: tool_result (success/failure)
    SDK->>Backend: stream tool_result
    Backend->>Backend: pending auth_request を completed/failed に更新、chat_auth_request 発行
    Backend->>WS: 更新イベント送信
    WS->>Front: ブロック更新を反映
    Front->>User: 完了/失敗表示(failureMessage表示)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Pull request title clearly summarizes the main changes: OAuth 2.1 pivot and introduction of auth_request UX, which are the primary objectives of this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oauth-pivot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 46 minutes and 40 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Premise 9 撤回: PAT only → MCP プロトコルの OAuth 2.1 採用 (user sovereignty)。

理由:
- Atlassian 公式 Rovo MCP は OAuth 2.1 ネイティブ、Claude Agent SDK は MCP の
  'needs-auth' status を扱う built-in support を持つ → SDK 任せで auto auth flow が回る
- sooperset/mcp-atlassian を使う場合も PAT は MCP server 自身の env で持つ前提に
- Tally プロセスから PAT/API key の概念が完全消滅 (project.yaml / メモリ / ログのいずれにも残らない)

実装変更:
- core: McpServerConfigSchema から auth フィールド削除、関連 hardening (envVar
  regex / Basic+Bearer 分岐) も削除。schema test を auth-less に整理
- ai-engine: buildMcpServers の auth header 構築ロジック削除、url のみで HTTP
  config (req は SDK が必要に応じて WWW-Authenticate を解釈)。chat-runner /
  agent-runner も auth に依存しない。env 未設定 throw test は仕様変更のため削除
- frontend: 設定 dialog から auth scheme dropdown / envVar 入力欄を削除、URL
  のみのシンプル UI、caption も「OAuth 2.1 / API token は MCP 任せ」に書き換え
外部 MCP の OAuth 認証フロー (authenticate / complete_authentication tool_use)
を assistant 文中の生 tool_use ではなく、専用カードで扱う。「Atlassian で認証」
ボタン (新規タブ) と callback URL paste 入力欄を統合。

- packages/core/src/schema.ts: ChatBlock に \`auth_request\` variant を追加
  (mcpServerId / mcpServerLabel / authUrl / status / failureMessage)。
  status と failureMessage の整合を superRefine で固定: failed なのに message
  無し / pending・completed に message が付いているケースを reject。
- packages/ai-engine/src/auth-detector.ts (新規): \`mcp__<id>__authenticate\` /
  \`complete_authentication\` パターンの分解、tool_result.output からの auth URL
  抽出。文末の句読点・括弧を URL に含めない処理 (`...state=xyz.` のような
  自然文出力で認可リンクが壊れるのを防ぐ)。
- packages/ai-engine/src/stream.ts: \`chat_auth_request\` event を ChatEvent に追加。
- packages/ai-engine/src/chat-runner.ts: tool_use を stash → tool_result とペアに
  なった瞬間に auth_request ブロックへ変換 (raw な tool_use/tool_result は出さない)。
  \`handleAuthToolResult\` / \`findLatestPendingAuthRequest\` を新設。
  complete_authentication 受領時は同 mcpServerId の最新 pending を更新。
- packages/frontend/src/components/chat/auth-request-card.tsx (新規):
  認証ボタン (新規タブ) と callback URL paste 入力欄を 1 カードに集約。
  callback URL は localhost / 127.0.0.1 のみ受け付ける軽い形式チェック付き。
- packages/frontend/src/components/chat/chat-message.tsx: auth_request →
  AuthRequestCard dispatch。
- packages/frontend/src/lib/store.ts: \`chat_auth_request\` event 反映 (pending
  append / completed・failed in-place 更新)。

- core/schema.test.ts: auth_request バリエーション 6 件 (基本 + 状態遷移 reject)
- ai-engine/auth-detector.test.ts: parseAuthToolName / extractAuthUrl 計 13 件
  (末尾句読点 strip 含む)
- ai-engine/chat-runner.test.ts: auth_request 変換フロー 3 件
  (authenticate / complete success / complete failure)
- frontend/auth-request-card.test.tsx: pending/completed/failed 描画と
  paste 検証 7 件

注: ChatRunner は per-turn sdk.query() のままで、long-lived Query 化は別 PR
(PR-C) で扱う。本 PR の auth_request UX 単体では「auth 1 回 → 同 thread の
次 turn で再 auth が必要」(SDK subprocess が turn ごとに再生成されるため
OAuth state が引き継がれない) という挙動を残す。
「mcpServers を空配列で全消去できる」テストの事前 PATCH の status
を assert していなかった。失敗していると後続の「空配列で削除」ケース
が偽の成功 (空 → 空) で通る恐れがあったため修正。
これまで failed/completed イベントは「同 mcpServerId の pending ブロック
を更新」する経路しか持たず、URL 抽出失敗等で pending を append する
前に failed が確定するケースで in-memory state に反映されなかった
(YAML には書かれるがリロードまで UI に出ない → 「何も起きない」と見える)。

evt.status==='failed' で同 mcpServerId の pending が見つからない場合、
pending と同様に該当 messageId に新規 append する。completed で
pending 不在は異常系のため無視 (現状維持)。

codex セカンドオピニオン (PR #21) Major M1 指摘対応。
…で埋める

callback URL paste 後の turn では SDK 応答が complete_authentication
tool_use/tool_result のみで、handleAuthToolResult は過去 message の
pending ブロックを更新するだけ。結果として現 turn の assistantMsgId
には何も append されず、空のアシスタント bubble が thread に蓄積し、
再生時の prompt にもノイズとして入り込む問題があった。

textBuffer が空 + 対象 message のブロックが 0 件なら、プレースホルダ
「(認証処理を完了しました)」テキストを replaceMessageBlocks で書く。

codex セカンドオピニオン (PR #21) Major M2 指摘対応。
… として受付

McpServerConfigSchema.url は loopback として localhost / 127.0.0.1 /
::1 / [::1] を許容しているが、AuthRequestCard 側の isLikelyCallbackUrl
は localhost / 127.0.0.1 のみ。IPv6 優先環境で SDK がリダイレクト先を
http://[::1]:XXXXX/callback?... で通知すると「認証完了」ボタンが永久
に無効になる可能性があった。schema.ts と loopback 判定を揃える。

codex セカンドオピニオン (PR #21) Minor m1 指摘対応。
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
packages/frontend/src/lib/store.ts (1)

385-470: ⚡ Quick win

chat_auth_request の更新処理は切り出したいです。

store.ts がすでにかなり大きく、ここにイベント分岐を足し続けると追跡とテスト追加がつらいです。applyAuthRequestEvent のような helper か別モジュールへ分けると、今回の pending / completed / failed 分岐も独立して検証しやすくなります。

As per coding guidelines, "Keep individual files under 500 lines; split larger files into multiple modules".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/src/lib/store.ts` around lines 385 - 470, The
chat_auth_request handling is too large in store.ts—extract it into a dedicated
helper (e.g. applyAuthRequestEvent) or a small module that receives (evt,
currentMessages) and returns updated messages; move the logic that inspects
evt.status ('pending' | 'completed' | 'failed'), searches/updates auth_request
blocks by mcpServerId/status, appends failed entries by evt.messageId when no
pending exists, and preserves existing block fields (mcpServerLabel, authUrl,
failureMessage) into that helper; update the store code to call
get().chatThreadMessages, pass them and evt to applyAuthRequestEvent, then call
set({ chatThreadMessages: result }) and keep the public behavior identical (use
the same symbols: evt, chatThreadMessages, blocks, mcpServerId, status,
failureMessage).
packages/frontend/src/components/chat/auth-request-card.test.tsx (1)

68-77: ⚡ Quick win

::1 許可のポジティブケースを追加してください。

Line 68-77 は拒否ケース中心で、IPv6 ループバック (http://[::1]:...) の許可を明示的に担保できていません。ここは回帰しやすい重要ロジックです。

追加テスト例
+  it('host が ::1 の callback URL は許可される', async () => {
+    const send = vi.fn().mockResolvedValue(undefined);
+    useCanvasStore.setState({ sendChatMessage: send } as never);
+    render(<AuthRequestCard block={pendingBlock} />);
+    const input = screen.getByRole('textbox', { name: /callback URL/ }) as HTMLInputElement;
+    fireEvent.change(input, {
+      target: { value: 'http://[::1]:54801/callback?code=AAA&state=xyz' },
+    });
+    const btn = screen.getByRole('button', { name: /^認証完了$/ }) as HTMLButtonElement;
+    expect(btn.disabled).toBe(false);
+    fireEvent.click(btn);
+    await screen.findByDisplayValue('');
+    expect(send).toHaveBeenCalledTimes(1);
+  });

Based on learnings: "Use Vitest for unit testing, and write tests for important logic before merging".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/src/components/chat/auth-request-card.test.tsx` around
lines 68 - 77, Test suite for AuthRequestCard lacks a positive case verifying
IPv6 loopback (::1) is accepted; add a unit test in
packages/frontend/src/components/chat/auth-request-card.test.tsx that mirrors
the existing negative case but sets the input value to an IPv6 loopback callback
(e.g. "http://[::1]:3000/callback?code=AAA&state=xyz"), ensure
useCanvasStore.setState({ sendChatMessage: vi.fn() } as never) and
render(<AuthRequestCard block={pendingBlock} />) as in the other test, then
assert the submit button (screen.getByRole('button', { name: /^認証完了$/ })) is
enabled (disabled === false) to confirm AuthRequestCard allows ::1 hosts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ai-engine/src/stream.ts`:
- Around line 63-71: The current chat_auth_request event type (type:
'chat_auth_request') makes failureMessage optional even when status is 'failed';
change the type into a discriminated union on the status field so that when
status === 'failed' failureMessage is required and when status is 'pending' |
'completed' failureMessage is absent/optional. Update the definition in
packages/ai-engine/src/stream.ts (the object with type: 'chat_auth_request',
messageId, mcpServerId, mcpServerLabel, authUrl, status, failureMessage) to
split into at least two interfaces/unions keyed by status and adjust any code
that references this event type to use the new union.

In `@packages/frontend/src/components/chat/auth-request-card.test.tsx`:
- Around line 30-37: The test currently creates a spy on window.open (openSpy)
and restores it inside the test body, which can leak if the test fails; change
the setup so window.open is spied in the test but its restoration is done in a
shared afterEach teardown: create the spy (vi.spyOn(window,
'open').mockImplementation(() => null)) in the test or a beforeEach, and move
the cleanup to an afterEach that calls openSpy.mockRestore() (or call
vi.restoreAllMocks()) to ensure the mock is always removed; reference the
openSpy/window.open usage in the test that renders AuthRequestCard and fires the
click.

In `@packages/frontend/src/components/chat/auth-request-card.tsx`:
- Around line 55-62: The message embeds block.mcpServerId in free text which
lets the model pick the wrong tool; change the send path so mcpServerId is sent
as a structured field/action instead of natural language: update the call site
that invokes sendChatMessage (in auth-request-card.tsx) to include a dedicated
payload or action property (e.g., { type: "complete_authentication",
mcpServerId: block.mcpServerId, url: trimmed, label: block.mcpServerLabel }) so
the agent receives an explicit mcpServerId the chat runner
(packages/ai-engine/src/chat-runner.ts) can match to pending blocks rather than
parsing it from the message body.
- Around line 15-27: The isLikelyCallbackUrl function currently accepts URLs
that include credentials (e.g. http://user:pass@localhost:54801/...), so update
isLikelyCallbackUrl to reject any URL with non-empty URL.username or
URL.password by checking u.username === '' && u.password === '' before returning
true; keep the existing protocol, host, and searchParams checks (function name:
isLikelyCallbackUrl) so credentialed callback URLs are not considered valid and
thus won't reach sendChatMessage.

In `@packages/frontend/src/lib/store.ts`:
- Around line 414-439: The code currently updates the first matching pending
auth_request by iterating messages/blocks forwards; change the traversal so you
locate and update the latest pending one by scanning messages and each
message.blocks from the end toward the start (i.e., reverse order) and stop
after the first match. In the implementation around updatedMessages/messages.map
and the inner m.blocks.map (and the b checks for b.type==='auth_request' &&
b.mcpServerId===evt.mcpServerId && b.status==='pending'), iterate blocks in
reverse (or find lastIndex) to set status/failureMessage on that single latest
block, then rebuild the blocks array preserving original order and mark
messageUpdated so you replace only the affected message.

---

Nitpick comments:
In `@packages/frontend/src/components/chat/auth-request-card.test.tsx`:
- Around line 68-77: Test suite for AuthRequestCard lacks a positive case
verifying IPv6 loopback (::1) is accepted; add a unit test in
packages/frontend/src/components/chat/auth-request-card.test.tsx that mirrors
the existing negative case but sets the input value to an IPv6 loopback callback
(e.g. "http://[::1]:3000/callback?code=AAA&state=xyz"), ensure
useCanvasStore.setState({ sendChatMessage: vi.fn() } as never) and
render(<AuthRequestCard block={pendingBlock} />) as in the other test, then
assert the submit button (screen.getByRole('button', { name: /^認証完了$/ })) is
enabled (disabled === false) to confirm AuthRequestCard allows ::1 hosts.

In `@packages/frontend/src/lib/store.ts`:
- Around line 385-470: The chat_auth_request handling is too large in
store.ts—extract it into a dedicated helper (e.g. applyAuthRequestEvent) or a
small module that receives (evt, currentMessages) and returns updated messages;
move the logic that inspects evt.status ('pending' | 'completed' | 'failed'),
searches/updates auth_request blocks by mcpServerId/status, appends failed
entries by evt.messageId when no pending exists, and preserves existing block
fields (mcpServerLabel, authUrl, failureMessage) into that helper; update the
store code to call get().chatThreadMessages, pass them and evt to
applyAuthRequestEvent, then call set({ chatThreadMessages: result }) and keep
the public behavior identical (use the same symbols: evt, chatThreadMessages,
blocks, mcpServerId, status, failureMessage).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 21cf645b-ec7b-4be0-947a-6cc95adf8dca

📥 Commits

Reviewing files that changed from the base of the PR and between 8713429 and 6875149.

📒 Files selected for processing (17)
  • packages/ai-engine/src/agent-runner.test.ts
  • packages/ai-engine/src/auth-detector.test.ts
  • packages/ai-engine/src/auth-detector.ts
  • packages/ai-engine/src/chat-runner.test.ts
  • packages/ai-engine/src/chat-runner.ts
  • packages/ai-engine/src/mcp/build-mcp-servers.test.ts
  • packages/ai-engine/src/mcp/build-mcp-servers.ts
  • packages/ai-engine/src/stream.ts
  • packages/core/src/schema.test.ts
  • packages/core/src/schema.ts
  • packages/frontend/src/app/api/projects/[id]/route.test.ts
  • packages/frontend/src/components/chat/auth-request-card.test.tsx
  • packages/frontend/src/components/chat/auth-request-card.tsx
  • packages/frontend/src/components/chat/chat-message.tsx
  • packages/frontend/src/components/dialog/project-settings-dialog.test.tsx
  • packages/frontend/src/components/dialog/project-settings-dialog.tsx
  • packages/frontend/src/lib/store.ts

Comment thread packages/ai-engine/src/stream.ts
Comment thread packages/frontend/src/components/chat/auth-request-card.test.tsx
Comment thread packages/frontend/src/components/chat/auth-request-card.tsx
Comment thread packages/frontend/src/components/chat/auth-request-card.tsx Outdated
Comment thread packages/frontend/src/lib/store.ts Outdated
CodeRabbit (PR #21) Major 4 件 + Minor 1 件への対応。

[Heavy lift / Major #4] OAuth callback を構造化 WS メッセージで送る
これまで callback URL paste 後に「[OAuth callback for ${mcpServerId}] ...」
という自然文を sendChatMessage で送り、AI に mcpServerId を解釈させて
mcp__<id>__complete_authentication を呼ばせていた。複数 MCP server を
同時運用する状況で AI が別 server の complete_authentication を選び、
元の auth_request カードが pending のまま残るリスクがあった。

UI から WS の構造化メッセージ {type:'oauth_callback', mcpServerId,
callbackUrl} を送り、サーバ側で確定的に「指定 server の
complete_authentication を呼べ」とプロンプトを生成する設計に変更。

- ai-engine/server.ts: ChatOAuthCallbackSchema 追加、handler 分岐
- ai-engine/chat-runner.ts: runOAuthCallback メソッド追加 (runUserTurn ラッパー)
- frontend/lib/ws.ts: sendOAuthCallback 追加
- frontend/lib/store.ts: sendOAuthCallback action 追加
- frontend/auth-request-card.tsx: sendChatMessage → sendOAuthCallback
- 関連 test 7 件を新 API に追従

[Quick win / Major #1] chat_auth_request を discriminated union 化
ai-engine/stream.ts: status='failed' のときのみ failureMessage を必須に
する型分割。schema.ts (AuthRequestBlockSchema) の superRefine と型レベル
で整合させ、永続化時の検証エラーを型チェックで早期検出。chat-runner の
emit 側と test の type narrowing も追従。

[Quick win / Major #3] callback URL の username/password 拒否
auth-request-card.tsx: isLikelyCallbackUrl で URL 内資格情報 (user:pass@)
を弾く。schema.ts (McpServerConfigSchema.url) と整合。

[Quick win / Major #5] 最新の pending auth_request を更新
frontend/lib/store.ts: chat_auth_request reducer の走査を末尾起点に変更。
再認証で複数 pending が並ぶケースで、古いカードを更新して新しいカードが
pending のまま残る問題を解消。

[Quick win / Minor #2] window.open spy の cleanup を afterEach に
auth-request-card.test.tsx: vi.restoreAllMocks() を afterEach に集約し、
テスト失敗時に spy が漏れて後続に影響する経路を塞ぐ。

[見送り / Minor (codex)] auth-detector.ts の id 命名規則 ADR
codex セカンドオピニオン Minor m2 (regex / schema 二重メンテの ADR メモ) は
本 PR スコープ外として見送り。
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ai-engine/src/chat-runner.ts`:
- Around line 373-383: runOAuthCallback currently delegates to runUserTurn which
persists the raw callback text to history and allows normal tool/model flow;
instead implement an ephemeral OAuth-only execution path: replace the yield*
this.runUserTurn(text, []) call in runOAuthCallback with logic that emits the
user-facing event(s) without saving to conversation history and then invokes
only the whitelist tool named `mcp__${mcpServerId}__complete_authentication`
(reject/ignore any other tool/model actions). Use or add a helper such as an
ephemeral runner (e.g., runEphemeralTurn/runOAuthTurn) or inline the behavior to
mark messages as non-persistent and constrain tool execution to that single tool
name so callback `code`/`state` are never stored and no other MCP server
completions can run.

In `@packages/ai-engine/src/server.ts`:
- Around line 51-55: ChatOAuthCallbackSchema's callbackUrl currently uses
z.string().url() which is too permissive; update the schema to validate
callbackUrl more strictly to match the front-end rules: ensure the URL scheme is
either http or https, reject URLs with username/password (no credentials),
restrict the host to loopback addresses (localhost, 127.0.0.1, ::1 / [::1]), and
require that the query string contains both code and state parameters. Implement
this by replacing the simple .url() check on callbackUrl in
ChatOAuthCallbackSchema with a custom zod refinement or parsed-URL validator
that parses the URL, checks the protocol, hostname, absence of auth, and
presence of code and state query params while preserving the existing type
shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6dd10985-750c-46f9-a792-7177e3288074

📥 Commits

Reviewing files that changed from the base of the PR and between 6875149 and 6057ac4.

📒 Files selected for processing (8)
  • packages/ai-engine/src/chat-runner.test.ts
  • packages/ai-engine/src/chat-runner.ts
  • packages/ai-engine/src/server.ts
  • packages/ai-engine/src/stream.ts
  • packages/frontend/src/components/chat/auth-request-card.test.tsx
  • packages/frontend/src/components/chat/auth-request-card.tsx
  • packages/frontend/src/lib/store.ts
  • packages/frontend/src/lib/ws.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ai-engine/src/stream.ts

Comment thread packages/ai-engine/src/chat-runner.ts Outdated
Comment thread packages/ai-engine/src/server.ts
CodeRabbit (PR #21) 4 周目 Major 2 件対応。

[Major #1 / Heavy lift] runOAuthCallback の ephemeral 化
これまで runOAuthCallback は \`yield* this.runUserTurn(text, [])\` を
呼んでおり、callback URL (code/state 含む) が user message として永続化
され、後続の prompt 再構築でも再送される問題があった。さらに通常 turn の
ままなので、AI が他のツール (create_node 等) を実行する余地が残っていた。

専用の ephemeral 実行経路に切替:
- chatStore.appendMessage を呼ばない (callback URL を chat 履歴に残さない)
- assistantMsgId は ephemeral (handleAuthToolResult の orphan 失敗 fallback
  以外では参照されない)
- 対象 server の config のみ buildMcpServers に渡す
- allowedTools = [\`mcp__<id>__complete_authentication\`] の 1 件のみ
- tool_use は kind === 'complete_authentication' && id 一致のみ stash
- text block と stash 不一致の tool_result は emit せず黙って捨てる
- handleAuthToolResult が過去 message の最新 pending を更新する経路は維持

[Major #2] サーバ側 callbackUrl 検証強化
ChatOAuthCallbackSchema.callbackUrl が \`z.string().url()\` のみで、
loopback / no-credentials / code+state 必須を確認していなかった。
WS は UI 経由でない外部クライアントからも到達しうるので、isValidCallbackUrl
ヘルパで isLikelyCallbackUrl (UI 側) と同じ条件をサーバ側でも検証する。
@shomatan shomatan merged commit 9468479 into main May 1, 2026
2 checks passed
@shomatan shomatan deleted the feat/oauth-pivot branch May 1, 2026 04:04
shomatan added a commit that referenced this pull request May 1, 2026
…保持) (PR-C、stacked on #21) (#22)

* refactor(chat): ChatRunner を long-lived Query 化 + codex 指摘 4 件対応

PR-C の主機能: 1 ChatRunner = 1 sdk.query() = 1 subprocess に固定して、
MCP HTTP transport の OAuth 状態 (PKCE / token) を turn 跨ぎで保持する。

main 最新 (PR-A の MCP 統合 + PR-B の OAuth 2.1 / auth_request UX +
私の修正多数: XML escape / externalToolUseIds / ephemeral runOAuthCallback
/ 空 assistant プレースホルダ等) と統合した形で再実装。

## 新規ファイル

- packages/ai-engine/src/async-input.ts: SDK の streaming input mode 用
  AsyncIterable<SdkUserMessage> 実装。push 可能 + close 即時 teardown
  (CR Major 反映: close 後 next() 即 done / FIFO waiter キュー)
- packages/ai-engine/src/async-input.test.ts: 8 件 (push / waiter / close /
  FIFO / close 後 即 done)

## agent-runner.ts

- SdkLike.query を `prompt: string | AsyncIterable<SdkUserMessageLike>` に拡張
- SdkUserMessageLike + SdkQueryHandle (任意 close()) 型追加

## chat-runner.ts

新規フィールド:
- query / input / outputLoopDone / outputLoopFailed / currentTurn /
  cachedExternalConfigById
- ensureQueryInflight (Promise キャッシュ、再入ガード)

新規 interface:
- TurnState: 1 user turn の間だけ生きる mutable state
  (assistantMsgId / queue / textBuffer / stashedAuthUses /
  externalConfigById / externalToolUseIds)

新規メソッド:
- ensureQuery: 1 度だけ query 立ち上げ + 出力ループ起動。inflight
  Promise キャッシュで再入ガード (codex Major 3)
- startQueryInternal: 内部の起動本体
- runOutputLoop: SDK message を currentTurn の queue に振り分け
- dispatchSdkMessage: 1 メッセージ処理 (text/tool_use/tool_result/result)
- tearDownQuery / close: リソース cleanup。close は outputLoopDone を退避
  してから tearDownQuery を呼ぶ (codex 致命 Minor: close 順序)

挙動変更:
- runUserTurn: 入口で currentTurn ガード (codex Major 1: turn 並走防止)
- input null チェックを invariant assertion で表明 (codex Major 2)
- 空 assistant message 永続化を ensureQuery 前に移動 (long-lived では
  bg loop が即起動するため、後置きだと race で空 message 残る)
- ephemeral runOAuthCallback も long-lived query 経由に統合
  (callback URL は input.push 経由で SDK に渡るが chatStore には
  永続化されない)

main 最新の機能を保持:
- auth-detector / handleAuthToolResult / stashedAuthUses
- externalToolUseIds (TurnState に統合)
- escapeXmlText / escapeXmlAttr (buildChatPrompt)
- 空 assistant プレースホルダ「(認証処理を完了しました)」
  (dispatchSdkMessage の result 処理に統合)

buildMcpServer は 1 引数化、handler 内で this.currentTurn から
assistantMsgId / emit を動的解決 (1 度作った MCP サーバを turn 跨ぎ
で使い回せる、turn 中は不変)。

## server.ts

- WS close 時に runner.close() を呼ぶ。close 内部の reject は
  .catch で warn (codex Major: void close() で unhandled rejection 化を回避)

## chat-runner.test.ts

- startCapturePromptText helper を追加: prompt が AsyncIterable<...>
  に変わったので、最初に push された user message の content を
  奪取する (string にも互換)
- 既存 test の prompt キャプチャ 4 箇所を helper 経由に書き換え

## テスト

pnpm typecheck: 4/4 PASS
pnpm test: core 93 + storage 88 + ai-engine 213 + frontend 273 = 667 PASS
pnpm lint: exit 0

* fix(chat): CR 5 周目 Major 1 + Minor 1 対応 (Major 1 は見送り)

CodeRabbit (PR #22) 1 周目への対応。

[Minor / chat-runner.ts:246] ensureQuery 失敗時の空 assistant ロールバック
long-lived Query 化に伴い空 assistant message を ensureQuery 前に append
するよう変更したが、ensureQuery 失敗時に空バブルが履歴に残る問題があった。
chatStore に message 単位の delete API が無いため、catch 内で
replaceMessageBlocks でエラー内容を blocks に書き込む形でロールバックする。

[Major / chat-runner.ts:361] 正常 EOF と subprocess 終了の区別
runOutputLoop の正常 EOF 経路では従来 chat_turn_ended のみを emit していたが、
これだと「予期しない subprocess 終了」も「明示 shutdown」も区別できず、
途中で切れた turn が正常完了に見えてしまう問題があった。
ChatRunner.isClosing フラグを追加し、close() 内で立てる。runOutputLoop の
EOF ブランチで isClosing が false なら agent_failed event を先に emit してから
chat_turn_ended で turn を閉じる。

[Major / chat-runner.ts:599] OAuth callback の long-lived 経路統合
(見送り) callback URL を this.input に push することで会話 context に
turn 跨ぎで残る、allowedTools 単一制約が prompt 依存になる、という懸念。

PR-C の主目的 (OAuth state を turn 跨ぎで保持) と SDK 制約のトレードオフ。
ephemeral 化すると subprocess 分離で state が引き継がれず再認証が必要になる。
本 PR では long-lived 維持を選択し、prompt 内で「他 tool は呼ぶな」と明示する
形でモデル依存の防御を入れる。SDK の MCP transport が状態共有 API を提供する
までは追加対応見送り。CR thread に詳細を返信。

* fix(chat): CR 2 周目 Major 4 件対応 (Quick win)

CodeRabbit (PR #22) 2 周目への対応。Heavy lift #3 (OAuth callback の
ephemeral 化) は前 PR と同様 long-lived 維持で見送り、CR thread に返信。

[Major / chat-runner.ts:349] close と ensureQuery の競合
close() が startQueryInternal の途中 (await projectStore.getProjectMeta() 等)
で走ると、shutdown 後に sdk.query() が作られて subprocess が孤立する race
があった。close() で ensureQueryInflight を join してから tearDown する。

[Major / chat-runner.ts:358] SDK メッセージの常時ログによる機密漏洩
runOutputLoop の console.log が全 SDK メッセージを redactMcpSecrets でラップ
してログしていたが、message.content や result の OAuth callback URL の
code/state がサーバーログに残るリスクがあった。type だけ出す形に絞り、
本文は出さない。redactMcpSecrets の import も削除。

[Major / chat-runner.ts:386] 異常終了後の pendingApprovals 残存
runOutputLoop の catch / EOF ブランチで queue を閉じるだけだったので、
turn 失敗後に UI から approveTool() が来ると create_node / create_edge 等
の side effect が走る恐れがあった。rejectAllPendingApprovals helper を
新設し、両ブランチで pendingApprovals を一括 reject(false) する。

[Major / chat-runner.ts:625] runOAuthCallback の currentTurn 解放保証
this.currentTurn = turnState の直後から全体を try/finally で囲み、
appendMessage 等の中間ステップで throw しても currentTurn が解放される
ことを保証 (turn_in_progress で次の turn を受け付けられなくなるのを防ぐ)。
runUserTurn 側も同じパターンで全体を try/finally に統一。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant